<template>
	<view v-if="enable"
		class="vi_imageEnlarge"
		:class="{'vi_imageEnlarge_active':showEnlarge}"
		:style="mainStyle"
		@touchmove.stop.prevent
		@click="close">
		<!-- 图片描述 -->
		<view class="vi_imageEnlarge_describe" :class="desClass||''" :style="desSize" v-if="describe" @click.stop>
			<scroll-view scroll-y class="vi_imageEnlarge_content" :style="desStyle" @touchmove.stop>
				<view v-html="describe"></view>
			</scroll-view>
			<view class="vi_imageEnlarge_btnDown iconfont" :class="iconSetting.down" @touchmove="slideMove" @touchstart="slideStart" @click.top="slideClick"></view>
		</view>
		<!-- 图片区 -->
		<movable-area class="vi_imageEnlarge_area" @click="fameClick">
			<movable-view class="vi_imageEnlarge_area-view"
				:style="movableViewStyle"
				direction="all"
				:scale="true"
				:damping="damping"
				:animation="setting.animation"
				:scale-max="setting.max"
				:scale-min="setting.min"
				:scale-value="syncS"
				:x="syncX"
				:y="syncY"
				@scale="onScale"
				@change="onChange">
				<view class="vi_imageEnlarge_imageArea" @click.stop>
					<image class="vi_imageEnlarge_image" :class="{'vi_imageEnlarge_turn':toolbarSetting.turn&&setting.animation}"
						:src="dataSet||''"
						:style="{transform: 'rotateZ(' + degree + 'deg)'}"
						mode="widthFix"
						@load="getImageInfo"
						@touchmove="touchMove"
						@touchstart="touchStart"
						@touchend="touchEnd"/>
				</view>
			</movable-view>
		</movable-area>
		<!-- 工具条 -->
		<view class="vi_imageEnlarge_toolbar" :style="toolbarStyle" @click.stop>
			<view class="vi_imageEnlarge_group" v-if="ready && toolbarSetting.save">
				<text class="vi_imageEnlarge_btn iconfont" :class="iconSetting.save" @click="save"></text>
			</view>
			<view class="vi_imageEnlarge_group" v-if="ready && useGroup">
				<text class="vi_imageEnlarge_btn iconfont" @click="restore"
					:class="[iconSetting.restore, {'disabled':!hasChange}]"></text>
					
				<text class="vi_imageEnlarge_btn iconfont" @click="scale('up')"
					:class="iconSetting.enlarge"
					v-if="toolbarSetting.scale"></text>
					
				<text class="vi_imageEnlarge_btn iconfont" @click="scale('down')"
					:class="iconSetting.shrink"
					v-if="toolbarSetting.scale"></text>
				
				<text class="vi_imageEnlarge_btn iconfont" @click="rotate('left')"
					:class="iconSetting.turnLeft"
					v-if="toolbarSetting.rotate"></text>
					
				<text class="vi_imageEnlarge_btn iconfont" @click="rotate('right')"
					:class="iconSetting.turnRight"
					v-if="toolbarSetting.rotate"></text>
				
				<text class="vi_imageEnlarge_btn iconfont" @click="rotate('turn')"
					:class="iconSetting.turn"
					v-if="toolbarSetting.turn"></text>
				
				<text class="vi_imageEnlarge_btn iconfont" @click="turnPage('prev')"
					:class="iconSetting.prev"
					v-if="toolbarSetting.page"></text>
					
				<text class="vi_imageEnlarge_btn iconfont" @click="turnPage('next')"
					:class="iconSetting.next"
					v-if="toolbarSetting.page"></text>
				
				<text class="vi_imageEnlarge_btn iconfont" @click="customEvent(item)"
					:class="item.icon||'vi_imageEnlarge_text'"
					v-for="(item, index) in (buttonCustom||[])" :key="index">
						{{item.icon?"":item.name||"按钮"}}
					</text>
			</view>
			<view class="vi_imageEnlarge_group" v-if="!setting.maskClick">
				<text class="vi_imageEnlarge_btn iconfont" :class="iconSetting.close" @click="close"></text>
			</view>
		</view>
	</view>
</template>

<script>
	export default {
		name:"viImageEnlarge",
		props:{
			//背景色
			bgColor:{
				type:[String,null],
				default:""
			},
			//图片地址
			dataSet:{
				type:[String,null],
				default:""
			},
			//初始放大倍数
			value:{
				type:[Number,null],
				default:null
			},
			//缩放最小值
			min:{
				type:[Number,null],
				default:null
			},
			//缩放最大值
			max:{
				type:[Number,null],
				default:null
			},
			//缩放步阶，最多1位小数
			step:{
				type:[Number,null],
				default:null
			},
			//屏幕适配，旋转后放大到满屏，初始进入满屏
			screenFix:{
				type:[Boolean,null],
				default:null
			},
			//如果观测到位移异常，可尝试关闭动画
			animation:{
				type:[Boolean,null],
				default:null
			},
			//图片预留间隙，默认单位rpx，可选px，不支持其他单位
			space:{
				type:[Number,String,null],
				default:0
			},
			//手动工具条，[scale, rotate(screen), page, save]
			toolbar:{
				type:[Array,Object,null],
				default:()=>[]
			},
			//工具条按钮大小，可带单位
			buttonSize:{
				type:[String,null],
				default:""
			},
			//定义按钮图标
			icon:{
				type:[Object,null],
				default:()=>({})
			},
			//自定义按钮，icon：iconfont图标名，event：点击向外抛出的事件名，text按钮文字（图标优先），close按钮点击后是否关闭
			buttonCustom:{
				type:[Array,null],
				default:()=>[
					// { icon:"", event:"", name:"", close:false }
				]
			},
			//自定义参数传递，在自定义按钮点击时向外提交
			params:{
				default:null
			},
			//长按点击触发时间
			longPressTime:{
				type:[Number,null],
				default:0
			},
			//点击遮罩关闭
			maskClick:{
				type:[Boolean,null],
				default:null
			},
			//图片描述
			describe:{
				type:[String,null],
				default:""
			},
			desClass:{
				type:[String,null],
				default:""
			},
			desFontSize:{
				type:[String,null],
				default:""
			},
			desMaxRow:{
				type:[Number,null],
				default:0
			},
			desLineHeight:{
				type:[Number,null],
				default:0
			},
			//下拉动作触发距离，单位px
			desTrigger:{
				type:[Number,null],
				default:0
			},
			//缩放时自动居中
			autoCenter:{
				type:[Boolean,null],
				default:null
			},
			//顶部偏移，用于使用自定义导航时
			top:{
				type:[String,null],
				default:""
			},
			zIndex:{
				type:[Number,null],
				default:null
			},
			statusTop:{
				type:[Number,null],
				default:0
			}
		},
		data() {
			return {
				enable:false,	//启用
				showEnlarge:false,	//显示
				duration:300,	//动画时长，须与css的transition变量一致
				openTimer:null,
				closeTimer:null,
				inValue:1,
				syncX:0,		//同步变更用，非当前位移
				syncY:0,		//同步变更用，非当前位移
				syncS:1,		//同步变更用，非当前缩放
				x:0,			//当前位移
				y:0,			//当前位移
				s:1,			//当前缩放
				imgW:60,
				imgH:60,
				degree:0,
				screen:{
					w:0,
					h:0,
					t:0
				},
				// #ifdef H5
				damping:100,		//观测到H5存在卡顿，给予较大值缩减动画
				// #endif
				// #ifndef H5
				damping:50,
				// #endif
				touchTimer:"",
				desOpened:false,
				desIsOpen:false,	//暂存变量，判断下拉触发时是否已打开
				desStart:0,			//暂存变量，下拉起始点y坐标
				ready:false,		//图片是否加载完成
				dicButton:{			//按钮定义
					scale: true,
					rotate: false,
					turn: true,
					page: false,
					save: false
				},
				dicIcon:{
					save: "icon-download",
					restore: "icon-restore",
					enlarge: "icon-enlarge",
					shrink: "icon-shrink",
					turnLeft: "icon-rotate-left",
					turnRight: "icon-rotate-right",
					turn: "icon-rotate-screen",
					prev: "icon-prev",
					next: "icon-next",
					close: "icon-close-circular",
					down: "icon-arr-double-down"
				},
			}
		},
		computed:{
			//将默认值内含到css、计算属性等地方，只内含真值
			//规避特殊使用的情况下，由prop传入undefined、null等无效数据导致的，默认值失效问题。例如组件嵌套且需要组件间参数传递
			//HBuilder X3.3.13; 微信开发者工具stable 1.05.2111300
			//微信小程序会对传入的undefined、null值根据参数类型输出不同空值(false、""、null、0)，参数类型为null或未指定参数类型则输出null
			//H5会对传入的undefined取默认值，未设默认值则输出undefined。对传入的null值固定输出null
			//做参数传递时需警惕两者行为不同而造成的陷阱，主要问题出现在微信小程序的空值转化，输出非用户传入的空值、假值，使默认真值丢失
			//兼容空值取默认值的可行方案目前为，参数类型加入null如[Boolean,null]，判断传入参数是否为定义的类型，是则使用，否则赋予默认值
			setting(){
				return {
					min: this.$api.isValue(this.min)? this.min :0.5,
					max: this.$api.isValue(this.max)? this.max :4,
					animation: this.$api.isBoolean(this.animation)? this.animation :true,
					maskClick: this.$api.isBoolean(this.maskClick)? this.maskClick :true,
					step: this.$api.isValue(this.step)? this.step :0.5,
					screenFix: this.$api.isBoolean(this.screenFix)? this.screenFix :true,
					buttonSize: this.buttonSize ||"44rpx",
					longPressTime: this.longPressTime ||1500,
					desMaxRow: this.desMaxRow ||4,
					desLineHeight: this.desLineHeight ||1.5,
					desTrigger: this.desTrigger ||30,
					autoCenter: this.$api.isBoolean(this.autoCenter)? this.autoCenter :true,
					zIndex: this.$api.isValue(this.zIndex)? this.zIndex :999,
				}
			},
			//工具按钮显示设置
			toolbarSetting(){
				let res ={};
				let toolbar = this.toolbar;
				let type = this.$api.typeAsk(toolbar);
				if(type=="Object"){
					Object.assign(res, this.dicButton, toolbar);
				}else if(type=="Array"){
					for(let key of toolbar){
						res[key] =true
					}
				}else{
					res = this.dicButton
				}
				if(res.turn){
					res["rotate"] = false
				}
				return res
			},
			useGroup(){
				for(let key of Object.keys(this.toolbarSetting)){
					if(key != "save" && this.toolbarSetting[key]){
						return true
					}
				}
				if(this.buttonCustom&&this.buttonCustom.length){
					return true
				}
				return false
			},
			movableViewStyle(){
				if(this.ready){
					let res = {
						width: this.imgW +'px',
						height: this.imgH +'px',
					}
					return this.$api.objToStr(res)
				}
				return "width:100vw;height:100vh;"
			},
			toolbarStyle(){
				return `font-size:${this.setting.buttonSize};`
			},
			mainStyle(){
				let res ={};
				res["background-color"] = this.bgColor?this.bgColor:"rgba(0,0,0,0.4)";
				res["z-index"] = this.setting.zIndex;
				return this.$api.objToStr(res)
			},
			desSize(){
				let res ={};
				this.desFontSize? res["font-size"] = this.desFontSize :"";
				let tStr = "1em";
				this.screen.t? tStr+=` + ${this.screen.t}px` :"";
				this.top? tStr+=` + ${this.top}` :"";
				res["padding-top"] = `calc(${tStr})`
				return this.$api.objToStr(res)
			},
			desStyle(){
				let MH = this.setting.desMaxRow * this.setting.desLineHeight +'em';
				let H = this.desOpened? MH : 0;
				return `line-height:${this.setting.desLineHeight}; max-height:${MH}; height:${H};`
			},
			//图片默认边距
			imageSpace(){
				if(this.$api.isNumber(this.space, true)){
					return uni.upx2px(this.space)
				}else if(String(this.space).endsWith("rpx")){
					return uni.upx2px(this.$api.splitUnit(this.space)[0])
				}else if(String(this.space).endsWith("px")){
					return this.$api.splitUnit(this.space)[0]
				}
				return 0
			},
			hasChange(){
				return this.inValue!=1 || this.degree!=0 || this.x!=this.centerPoint.x || this.y!=this.centerPoint.y
			},
			centerPoint(){
				return {
					x: Math.round((this.screen.w - this.imgW * this.inValue) /2),
					y: Math.round((this.screen.h - this.imgH * this.inValue) /2)
				}
			},
			iconSetting(){
				if(this.$api.isObject(this.icon)){
					return Object.assign({},this.dicIcon,this.icon)
				}
				return this.dicIcon
			}
		},
		methods: {
			fameClick(){
				if(this.setting.maskClick){
					this.close()
				}
			},
			open(){
				//打开中或已打开，退出
				if((this.enable&&this.openTimer)||this.showEnlarge){
					return
				}
				//如果有关闭计时器，清除
				if(this.closeTimer){
					clearTimeout(this.closeTimer);
					this.closeTimer =null;
				}
				this.enable = true;
				//延迟执行动画，否则不显示动画只闪烁
				setTimeout(()=>{
					this.showEnlarge = true;
					this.openTimer = setTimeout(()=>{
						this.openTimer = null;
						this.$emit("opened")
					},this.duration)
				},30)
				//下一帧提交变更事件，确保内容已创建
				this.$nextTick(()=>{
					this.$emit("change",true)
				})
			},
			close() {
				//关闭中或已关闭，退出
				if((!this.showEnlarge&&this.closeTimer)||!this.enable){
					return
				}
				//如果有打开计时器，清除
				if(this.openTimer){
					clearTimeout(this.openTimer);
					this.openTimer =null;
				}
				this.showEnlarge = false;
				//窗口关闭后再还原设置
				this.closeTimer = setTimeout(() => {
					this.enable = false;
					this.closeTimer = null;
					this.restore();
					this.$emit("closed");
				},this.duration)
			},
			//还原
			restore(){
				if(this.hasChange){
					this.degree = 0;
					if(this.inValue != 1){
						this.syncS = this.inValue;
						this.$nextTick(()=>{
							this.syncS = this.inValue = 1;
							this.toCenter(true);
						})
					}else{
						this.toCenter(true);
					}
				}
			},
			//获取屏幕信息，如果需要监听屏幕翻转，另行设置
			getScreenInfo(){
				try {
					const res = uni.getSystemInfoSync();
					this.screen ={
						w: res.windowWidth,
						h: res.windowHeight + (res.windowTop||0),
						t: this.statusTop || res.safeArea.top
					}
				} catch (e) {
					console.log("获取系统信息失败");
				}
			},
			//获取图片比例
			getImageInfo(e){
				// console.log("图片信息",e);
				let w = e.detail.width ||360,
					h = e.detail.height ||360;
				let winRatio = this.screen.w / this.screen.h;
				let imgRatio = w / h;
				if(imgRatio >= winRatio){//横图，宽减去间隙
					this.imgW = parseInt(this.screen.w - this.imageSpace *2);
					this.imgH = parseInt(this.imgW * h / w);
				}else{//竖图，高减去间隙
					this.imgH = parseInt(this.screen.h - this.imageSpace *2);
					this.imgW = parseInt(this.imgH * w / h);
				}
				let centerX = Math.round((this.screen.w - this.imgW * this.inValue) /2),
					centerY = Math.round((this.screen.h - this.imgH * this.inValue) /2);
				this.syncX = this.x = centerX;
				this.syncY = this.y = centerY;
				//第一次加载重置比例，小程序莫名被更改
				if(!this.ready){
					this.scale(this.value ||1)
				}
				this.ready = true;
			},
			//图片缩放，可外部使用
			scale(type){
				this.syncS = this.inValue;
				let step = this.setting.step;
				if(type == 'up'){//值改变只能执行一次，会触发动画，不要用表达式简写
					let max = this.setting.max;
					if(this.inValue == max){
						return
					}else if(this.inValue + step > max - 0.1){
						this.inValue = max
					}else{
						this.inValue = this.$api.numberFix(this.inValue + step, 1)
					}
				}else if(type == 'down'){
					let min = this.setting.min;
					if(this.inValue == min){
						return
					}else if(this.inValue - step < min + 0.1){
						this.inValue = min
					}else{
						this.inValue = this.$api.numberFix(this.inValue - step, 1)
					}
				}else if(this.$api.isNumber(type, true)){
					this.inValue = Number(type);
				}else{
					return
				}
				this.$nextTick(()=>{
					this.syncS = this.inValue;
					if(this.setting.autoCenter){
						this.toCenter();
					}
				})
			},
			//图片旋转
			rotate(type) {
				switch(type){
					case "left":
						this.degree -= 90;
						this.degree = this.degree < 0 ? this.degree + 360 : this.degree;
						break;
					case "right":
						this.degree += 90;
						this.degree = this.degree >= 360 ? this.degree - 360 : this.degree;
						break;
					case "turn":
					default:
						this.degree = this.degree == 0 ? 90 : 0;
				}
				if(this.setting.screenFix){//满屏适配，以宽度为计算标准，预留间隙
					this.syncS = this.inValue;
					if(this.degree%180 == 90){//翻转
						let winRatio = this.screen.w / this.screen.h;
						let imgRatio = this.imgH / this.imgW;
						if(imgRatio >= winRatio){//图片高溢出，屏幕宽作为变化后的图片高
							this.inValue = this.$api.numberFix((this.screen.w - this.imageSpace *2) / this.imgH, 1);
						}else{
							this.inValue = this.$api.numberFix((this.screen.h - this.imageSpace *2) / this.imgW, 1);
						}
					}else{//还原
						this.inValue = 1;
					}
					this.$nextTick(()=>{
						this.syncS = this.inValue;
						this.toCenter();
					})
				}
			},
			//翻页，只提交事件，外部处理
			turnPage(type){
				this.$emit(type)
			},
			//保存
			save() {
				let _this = this;
				let src = this.dataSet||"";
				//#ifdef MP-WEIXIN
				//提前向用户发起授权请求
				uni.authorize({
					scope: 'scope.writePhotosAlbum',
					success: function(){
						//允许储存
						_this.downloadImg(src);
					}
				});
				//#endif
			
				//#ifdef H5
				//非同源图片将直接打开
				let atag = document.createElement('a');
				atag.href = src;
				atag.download = '';
				atag.target = '_blank';
				atag.click();
				//#endif
			},
			//下载并保存文件
			downloadImg(src) {
				//下载图片文件
				uni.showLoading({
					title: '图片提取中'
				});
				uni.downloadFile({
					url: src,
					success: (res) => {
						uni.hideLoading();
						uni.saveImageToPhotosAlbum({
							filePath: res.tempFilePath,
							success: () => {
								uni.showToast({
									title: '已保存至相册',
									duration: 1000
								});
							}
						});
					},
					fail: (e) => {
						console.log("图片下载失败", e);
						uni.hideLoading();
						uni.showToast({
							title: '图片下载失败',
							icon: 'none',
							duration: 1000
						});
					}
				});
			},
			//自定义按钮事件
			customEvent(item){
				if(item.event){
					this.$emit("event", {
						"event": item.event,
						"image": this.dataSet,
						"params": this.params
					})
				}
				if(item.close){
					this.close()
				}
			},
			//图片触屏开始
			touchStart(e){
				if (e.touches.length != 1) {
					return false;
				}
				this.touchTimer = setTimeout(() => {
					this.onLongPress();
					this.touchTimer = null;
				}, this.setting.longPressTime);
				return false;
			},
			//图片触屏移动
			touchMove(e) {
				if (e.touches.length != 1) {
					return false;
				}
				clearTimeout(this.touchTimer);
			},
			//图片触屏结束
			touchEnd(){
				if(this.touchTimer){//存在定时器，未执行长按事件，判断为点击
					clearTimeout(this.touchTimer);
					this.$emit("click", this.dataSet)
				}
				//该次点击执行过长按事件则直接跳出
				return false;
			},
			//长按事件
			onLongPress() {
				this.$emit('longPress', this.dataSet);
			},
			//描述下拉开始事件
			slideStart(e){
				if (e.touches.length != 1) {
					return false;
				}
				this.desStart = e.touches[0].clientY;
				this.desIsOpen = this.desOpened;
			},
			slideMove(e){
				if (e.touches.length != 1) {
					return false;
				}
				let y = e.touches[0].clientY,
					sy = this.desStart;
				if(y>sy && y-sy>(this.setting.desTrigger)){
					this.desOpened = true
				}else if(y<sy && sy-y>(this.setting.desTrigger)){
					this.desOpened = false
				}
			},
			slideClick(){
				this.desOpened = !this.desOpened;
			},
			//位移变化记录
			onChange(e){
				this.x = Math.round(e.detail.x);
				this.y = Math.round(e.detail.y);
			},
			//比例变化记录
			onScale(e){
				this.inValue = Math.round(e.detail.scale *10) /10 ;
			}
		},
		created() {
			//图片居中函数，节流调用
			this["toCenter"] = this.$api.throttle((force=false)=>{
				//仅支持H5使用。观测到微信小程序在居中时x轴发生预料外偏移
				//H5同时变更scale、x、y，同样存在偏移问题，但可用延时错位处理
				//问题版本：Hbuilder3.3.5，微信1.05.2111300
				let isH5;
				// #ifdef H5
				isH5 = true;
				// #endif
				if(isH5 || force){
					clearTimeout(this.toCenterTimer);
					this.toCenterTimer = setTimeout(()=>{
						this.syncX = this.x;
						this.syncY = this.y;
						this.$nextTick(()=>{
							this.syncX = this.x = this.centerPoint.x;
							this.syncY = this.y = this.centerPoint.y;
						})
					}, this.setting.animation? 30000 / this.damping :0 )
				}
			}, {type:"last"});
		},
		mounted() {
			//获取屏幕信息
			this.getScreenInfo();
			//初始让图片居中
			uni.onWindowResize(()=>{
				this.getScreenInfo();
			})
		},
		destroyed() {
			uni.offWindowResize(()=>{})
		}
	}
</script>

<style lang="scss">
$transition:.3s;
.vi_imageEnlarge{
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	opacity: 0;
	transition: opacity $transition;
	&.vi_imageEnlarge_active{
		opacity: 1;
	}
	.vi_imageEnlarge_area{
		width: 100%;
		height: 100%;
		overflow: hidden;
	}
	.vi_imageEnlarge_area-view{
		.vi_imageEnlarge_imageArea,
		.vi_imageEnlarge_image{
			width: 100%;
			height: 100%;
		}
		.vi_imageEnlarge_turn{
			transition: transform $transition;
		}
		.vi_imageEnlarge_imageArea{
			box-sizing: border-box;
		}
	}
	.vi_imageEnlarge_toolbar{
		display: flex;
		align-items: center;
		position: fixed;
		left: 50%;
		bottom: 20%;
		color: rgba(255,255,255,0.75);
		transform: translateX(-50%);
		.vi_imageEnlarge_group{
			display: flex;
			align-items: center;
			background-color: rgba(0,0,0,0.25);
			padding: 0.25em 0.5em;
			border-radius: 0.35em;
			margin-right: 1em;
			flex-shrink: 0;
			&:last-child{
				margin-right: 0;
			}
		}
		.vi_imageEnlarge_btn{
			margin-right: 0.5em;
			&:last-child{
				margin-right:0
			}
			&:active{
				color: rgba(255,255,255,1);
			}
			&.disabled{
				color: rgba(255,255,255,0.25);;
			}
			&.vi_imageEnlarge_text{
				font-size: 75%;
				width: auto;
			}
		}
	}
	.vi_imageEnlarge_describe{
		display: flex;
		flex-direction: column;
		align-items: center;
		position: fixed;
		top: 0;
		left: 0.25em;
		z-index: 1;
		width: calc(100% - 0.5em);
		padding: 1em 1em 0;
		box-sizing: border-box;
		background-color: rgba(0,0,0,0.25);
		border-radius: 0 0 0.5em 0.5em;
		color: #fff;
		transition: height $transition;
		.vi_imageEnlarge_content{
			transition: 0.3s;
			height: 0;
			overflow: auto;
		}
		.vi_imageEnlarge_btnDown{
			padding: 0 2em 0.75em;
			animation: vi-imageEnlarge-jump 0.5s infinite;
		}
	}
}
@keyframes vi-imageEnlarge-jump{
	0%{ transform: translateY(0); }
	50%{ transform: translateY(0.25em); }
	100%{ transform: translateY(0); }
}
</style>
